package controllers

/*
Copyright 2021-2025 The k8gb Contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Generated by GoLic, for more details see: https://github.com/AbsaOSS/golic
*/

import (
	"context"
	"fmt"
	"net"
	"reflect"

	"github.com/k8gb-io/k8gb/controllers/resolver"

	"github.com/k8gb-io/k8gb/controllers/providers/k8gbendpoint"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/k8gb-io/k8gb/controllers/utils"

	"github.com/k8gb-io/k8gb/controllers/providers/metrics"
	"github.com/k8gb-io/k8gb/controllers/refresolver"

	"errors"

	k8gbv1beta1 "github.com/k8gb-io/k8gb/api/v1beta1"
	"github.com/k8gb-io/k8gb/controllers/logging"
	"github.com/k8gb-io/k8gb/controllers/providers/dns"
	"go.opentelemetry.io/otel/trace"
	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// GslbReconciler reconciles a Gslb object
type GslbReconciler struct {
	client.Client
	Scheme             *runtime.Scheme
	Config             *resolver.Config
	Resolver           resolver.GslbResolver
	DNSProvider        dns.Provider
	Recorder           record.EventRecorder
	Tracer             trace.Tracer
	GslbIngressHandler Handler
}

const (
	primaryGeoTagAnnotation = "k8gb.io/primary-geotag"
	strategyAnnotation      = "k8gb.io/strategy"
	dnsTTLSecondsAnnotation = "k8gb.io/dns-ttl-seconds"
)

var k8gbAnnotations = []string{strategyAnnotation, primaryGeoTagAnnotation, dnsTTLSecondsAnnotation}

var log = logging.Logger()

var m = metrics.Metrics()

// +kubebuilder:rbac:groups=k8gb.absa.oss,resources=gslbs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=k8gb.absa.oss,resources=gslbs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

// Reconcile runs main reconciliation loop
func (r *GslbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	ctx, span := r.Tracer.Start(ctx, "Reconcile")
	defer span.End()

	result := utils.NewReconcileResultHandler(r.Config.ReconcileRequeueSeconds)
	// Fetch the Gslb instance
	gslb := &k8gbv1beta1.Gslb{}
	err := r.Get(ctx, req.NamespacedName, gslb)
	if err != nil {
		if k8serrors.IsNotFound(err) {
			// Request object not found, could have been deleted after reconcile request.
			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
			// Return and don't requeue
			return result.Stop()
		}
		m.IncrementError(gslb)
		return result.RequeueError(fmt.Errorf("error reading the object (%s)", err))
	}

	err = r.Resolver.ResolveGslbSpec(ctx, gslb, r.Client)
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(fmt.Errorf("resolving spec (%s)", err))
	}
	log.Debug().
		Str("gslb", gslb.Name).
		Str("namespace", gslb.Namespace).
		Str("strategy", gslb.Spec.Strategy.Type).
		Msg("Resolved strategy")

	// == Ingress ==========
	// Only create embedded Ingress if ResourceRef is empty (embedded mode)
	if reflect.DeepEqual(gslb.Spec.ResourceRef, k8gbv1beta1.ResourceRef{}) {
		ingress, err := r.createIngressFromGslb(gslb)
		if err != nil {
			m.IncrementError(gslb)
			return result.RequeueError(err)
		}

		err = r.saveDependentIngress(ctx, gslb, ingress)
		if err != nil {
			m.IncrementError(gslb)
			return result.RequeueError(err)
		}
	}

	// == Reference resolution ==
	refResolver, err := refresolver.New(gslb, r.Client)
	if err != nil {
		m.IncrementError(gslb)
		errorMsg := fmt.Sprintf("error resolving references (%s)", err)
		r.Recorder.Event(gslb, corev1.EventTypeWarning, "ReconcileError", errorMsg)
		return result.RequeueError(errors.New(errorMsg))
	}
	servers, err := refResolver.GetServers()
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(fmt.Errorf("getting GSLB servers (%s)", err))
	}
	gslb.Status.Servers = servers

	loadBalancerExposedIPs, err := refResolver.GetGslbExposedIPs(gslb.Annotations, r.Config.ParentZoneDNSServers)
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(fmt.Errorf("getting load balancer exposed IPs (%s)", err))
	}
	lbIpv4Addresses, _ := splitIPsByVersion(loadBalancerExposedIPs)
	gslb.Status.LoadBalancer.ExposedIPs = lbIpv4Addresses

	log.Debug().
		Str("gslb", gslb.Name).
		Msg("Resolved LoadBalancer and Server configuration referenced by Ingress")

	// == health status of applications ==
	serviceHealth, err := r.getServiceHealthStatus(ctx, gslb)
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(err)
	}
	gslb.Status.ServiceHealth = serviceHealth

	// == external-dns dnsendpoints CRs ==
	_, s := r.Tracer.Start(context.Background(), "gslbDNSEndpoint")
	epProvider := k8gbendpoint.NewApplicationDNSEndpoint(
		context.TODO(),
		r.Client,
		r.Config,
		gslb,
		log,
		utils.NewDNSQueryService(),
		r.updateRuntimeStatus)
	dnsEndpoint, err := epProvider.GetDNSEndpoint()
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(err)
	}
	err = controllerutil.SetControllerReference(gslb, dnsEndpoint, r.Scheme)
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(err)
	}
	s.End()

	_, s = r.Tracer.Start(ctx, "SaveDNSEndpoint")
	err = epProvider.SaveDNSEndpoint(dnsEndpoint)
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(err)
	}
	s.End()

	// == Status =
	err = r.updateGslbStatus(ctx, gslb, dnsEndpoint)
	if err != nil {
		m.IncrementError(gslb)
		return result.RequeueError(err)
	}

	// == Finish ==========
	// Everything went fine, requeue after some time to catch up
	// with external Gslb status
	// TODO: potentially enhance with smarter reaction to external Event
	m.IncrementReconciliation(gslb)
	return result.Requeue()
}

// splitIPsByVersion splits a list of IP addresses into IPv4 and IPv6 addresses
func splitIPsByVersion(ips []string) ([]string, []string) {
	ipv4Addresses := []string{}
	ipv6Addresses := []string{}
	for _, ip := range ips {
		parsedIP := net.ParseIP(ip)
		if parsedIP == nil {
			log.Warn().Str("ip", ip).Msg("Invalid IP address")
			continue
		}
		if parsedIP.To4() == nil {
			// cannot be parsed as IPv4, must be IPv6
			ipv6Addresses = append(ipv6Addresses, ip)
		} else {
			ipv4Addresses = append(ipv4Addresses, ip)
		}
	}
	return ipv4Addresses, ipv6Addresses
}
